import {
  filter,
  isNumber
} from 'min-dash';

var max = Math.max,
    min = Math.min;

var DEFAULT_CHILD_BOX_PADDING = 20;

import {
  getBBox
} from '../../util/Elements';

import {
  asTRBL,
  asBounds
} from '../../layout/LayoutUtil';

/**
 * @typedef {import('../../core/Types').ElementLike} Element
 * @typedef {import('../../core/Types').ShapeLike} Shape
 *
 * @typedef {import('../../util/Types').Direction} Direction
 * @typedef {import('../../util/Types').Point} Point
 * @typedef {import('../../util/Types').Rect} Rect
 * @typedef {import('../../util/Types').RectTRBL} RectTRBL
 */

/**
 * Substract a TRBL from another
 *
 * @param {RectTRBL} trblA
 * @param {RectTRBL} trblB
 *
 * @return {RectTRBL}
 */
export function substractTRBL(trblA, trblB) {
  return {
    top: trblA.top - trblB.top,
    right: trblA.right - trblB.right,
    bottom: trblA.bottom - trblB.bottom,
    left: trblA.left - trblB.left
  };
}

/**
 * Resize the given bounds by the specified delta from a given anchor point.
 *
 * @param {Rect} bounds the bounding box that should be resized
 * @param {Direction} direction in which the element is resized (nw, ne, se, sw)
 * @param {Point} delta of the resize operation
 *
 * @return {Rect} resized bounding box
 */
export function resizeBounds(bounds, direction, delta) {
  var dx = delta.x,
      dy = delta.y;

  var newBounds = {
    x: bounds.x,
    y: bounds.y,
    width: bounds.width,
    height: bounds.height
  };

  if (direction.indexOf('n') !== -1) {
    newBounds.y = bounds.y + dy;
    newBounds.height = bounds.height - dy;
  } else if (direction.indexOf('s') !== -1) {
    newBounds.height = bounds.height + dy;
  }

  if (direction.indexOf('e') !== -1) {
    newBounds.width = bounds.width + dx;
  } else if (direction.indexOf('w') !== -1) {
    newBounds.x = bounds.x + dx;
    newBounds.width = bounds.width - dx;
  }

  return newBounds;
}


/**
 * Resize the given bounds by applying the passed
 * { top, right, bottom, left } delta.
 *
 * @param {Rect} bounds
 * @param {RectTRBL} resize
 *
 * @return {Rect}
 */
export function resizeTRBL(bounds, resize) {
  return {
    x: bounds.x + (resize.left || 0),
    y: bounds.y + (resize.top || 0),
    width: bounds.width - (resize.left || 0) + (resize.right || 0),
    height: bounds.height - (resize.top || 0) + (resize.bottom || 0)
  };
}


export function reattachPoint(bounds, newBounds, point) {

  var sx = bounds.width / newBounds.width,
      sy = bounds.height / newBounds.height;

  return {
    x: Math.round((newBounds.x + newBounds.width / 2)) - Math.floor(((bounds.x + bounds.width / 2) - point.x) / sx),
    y: Math.round((newBounds.y + newBounds.height / 2)) - Math.floor(((bounds.y + bounds.height / 2) - point.y) / sy)
  };
}


function applyConstraints(attr, trbl, resizeConstraints) {

  var value = trbl[attr],
      minValue = resizeConstraints.min && resizeConstraints.min[attr],
      maxValue = resizeConstraints.max && resizeConstraints.max[attr];

  if (isNumber(minValue)) {
    value = (/top|left/.test(attr) ? min : max)(value, minValue);
  }

  if (isNumber(maxValue)) {
    value = (/top|left/.test(attr) ? max : min)(value, maxValue);
  }

  return value;
}

export function ensureConstraints(currentBounds, resizeConstraints) {

  if (!resizeConstraints) {
    return currentBounds;
  }

  var currentTrbl = asTRBL(currentBounds);

  return asBounds({
    top: applyConstraints('top', currentTrbl, resizeConstraints),
    right: applyConstraints('right', currentTrbl, resizeConstraints),
    bottom: applyConstraints('bottom', currentTrbl, resizeConstraints),
    left: applyConstraints('left', currentTrbl, resizeConstraints)
  });
}


export function getMinResizeBounds(direction, currentBounds, minDimensions, childrenBounds) {

  var currentBox = asTRBL(currentBounds);

  var minBox = {
    top: /n/.test(direction) ? currentBox.bottom - minDimensions.height : currentBox.top,
    left: /w/.test(direction) ? currentBox.right - minDimensions.width : currentBox.left,
    bottom: /s/.test(direction) ? currentBox.top + minDimensions.height : currentBox.bottom,
    right: /e/.test(direction) ? currentBox.left + minDimensions.width : currentBox.right
  };

  var childrenBox = childrenBounds ? asTRBL(childrenBounds) : minBox;

  var combinedBox = {
    top: min(minBox.top, childrenBox.top),
    left: min(minBox.left, childrenBox.left),
    bottom: max(minBox.bottom, childrenBox.bottom),
    right: max(minBox.right, childrenBox.right)
  };

  return asBounds(combinedBox);
}

function asPadding(mayBePadding, defaultValue) {
  if (typeof mayBePadding !== 'undefined') {
    return mayBePadding;
  } else {
    return DEFAULT_CHILD_BOX_PADDING;
  }
}

export function addPadding(bbox, padding) {
  var left, right, top, bottom;

  if (typeof padding === 'object') {
    left = asPadding(padding.left);
    right = asPadding(padding.right);
    top = asPadding(padding.top);
    bottom = asPadding(padding.bottom);
  } else {
    left = right = top = bottom = asPadding(padding);
  }

  return {
    x: bbox.x - left,
    y: bbox.y - top,
    width: bbox.width + left + right,
    height: bbox.height + top + bottom
  };
}


/**
 * Is the given element part of the resize
 * targets min boundary box?
 *
 * This is the default implementation which excludes
 * connections and labels.
 *
 * @param {Element} element
 */
function isBBoxChild(element) {

  // exclude connections
  if (element.waypoints) {
    return false;
  }

  // exclude labels
  if (element.type === 'label') {
    return false;
  }

  return true;
}

/**
 * Return children bounding computed from a shapes children
 * or a list of prefiltered children.
 *
 * @param {Shape|Shape[]} shapeOrChildren
 * @param {RectTRBL|number} padding
 *
 * @return {Rect}
 */
export function computeChildrenBBox(shapeOrChildren, padding) {

  var elements;

  // compute based on shape
  if (shapeOrChildren.length === undefined) {

    // grab all the children that are part of the
    // parents children box
    elements = filter(shapeOrChildren.children, isBBoxChild);

  } else {
    elements = shapeOrChildren;
  }

  if (elements.length) {
    return addPadding(getBBox(elements), padding);
  }
}
